﻿using System;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Runtime.InteropServices;
using System.Security;
using System.Threading;
using System.Windows.Forms;
using OpenCLNet;

namespace GPUCaster
{
    public partial class CasterForm : Form
    {
        private bool _initialized;
        private Bitmap _outputBitmap = new Bitmap(640, 480, PixelFormat.Format32bppArgb);
        private Volume _volume;
        private Float4 _cameraPosition;
        private float _cameraAngle;
        private float _cameraDistance = 1000;
        private Float4 _lightPosition = new Float4() {S0=1000, S1=- 500, S2=-400, S3=0};
        private Float4 _cameraLookAt;
        private Float4 _cameraForward;
        private const int OpenClBlocksize = 64; 
        private readonly Stopwatch _drawingStopwatch = new Stopwatch();
        private OpenCLManager _oclMan;
        private OpenCLNet.Program _oclProgram;
        private Kernel _rayCastKernel;
        private Float4 _boxmin = new Float4(0, 0, 0, 0);
        private Float4 _boxmax = new Float4(512, 512, 512, 0);
        private int _gradientType;
        private float _cameraXPositionFactor;
        private float _cameraYPositionFactor;
        private float _cameraZPositionFactor;
        private Point _oldPosition;        

        public CasterForm()
        {
            InitializeComponent(); 
            UpdateLightVectorLabel();
            UpdateBoundingBoxLabels();
        }

        private void Draw()
        {
            var bd = _outputBitmap.LockBits(new Rectangle(0, 0, _outputBitmap.Width, _outputBitmap.Height), ImageLockMode.WriteOnly, _outputBitmap.PixelFormat);
            DoRayCasting(bd);
            _outputBitmap.UnlockBits(bd);
        }

        private void LoadBunnyVolumeData()
        {
            _volume = new Volume(517, _oclMan);
            _boxmin = new Float4(0, 0, 0, 0);
            _boxmax = new Float4(512,512,512,0);

            for (var f = 1; f <= 361; f++)
            {
                var fs = new FileStream("ct/" + f, FileMode.Open, FileAccess.Read);

                var readbytes = 0;
                var buffer = new byte[2 * 512 * 512];
                while (readbytes < buffer.Length)
                {
                    readbytes += fs.Read(buffer, readbytes, buffer.Length - readbytes);
                }

                for (var y = 0; y < 512; y++)
                {
                    for (var x = 0; x < 512; x++)
                    {
                        var b = new byte[2];
                        b[1] = buffer[((x * 512 + y) * 2)];
                        b[0] = buffer[((x * 512 + y) * 2) + 1];
                        short i = BitConverter.ToInt16(b, 0);

                        if (i > 1024 && i < 3071)
                        {
                            var xx = (int)((x / 512.0) * (_volume.GetSize() - 5));
                            var yy = 512 - (int)((y / 512.0) * (_volume.GetSize() - 5));
                            var zz = (int)((f / 361.0) * 360);
                            _volume.SetValue(xx, 360 - zz, yy, i);
                        }

                    }
                }
                fs.Close();
                GC.Collect();
            }
            GC.Collect();

            _cameraXPositionFactor = 2;
            _cameraYPositionFactor = 2;
            _cameraZPositionFactor = 2;
        }

        private void LoadHeadVolumeData()
        {
            _volume = new Volume(335, _oclMan);
            _boxmin = new Float4(0, 0, 0, 0);
            _boxmax = new Float4(335, 335, 335, 0);

            for (var f = 1; f <= 113; f++)
            {
                var fs = new FileStream("CThead/CThead." + f, FileMode.Open, FileAccess.Read);

                var readbytes = 0;
                var buffer = new byte[2 * 256 * 256];
                while (readbytes < buffer.Length)
                {
                    readbytes += fs.Read(buffer, readbytes, buffer.Length - readbytes);
                }

                for (var y = 0; y < 256; y++)
                {
                    for (var x = 0; x < 256; x++)
                    {
                        var b = new byte[2];
                        b[1] = buffer[((x * 256 + y) * 2)];
                        b[0] = buffer[((x * 256 + y) * 2) + 1];
                        short i = BitConverter.ToInt16(b, 0);

                        if (i > 500 && i < 4000)
                        {
                            var xx = (int)((x / 256.0) * (257.0 - 5));
                            var yy = 256 - (int)((y / 256.0) * (257.0 - 5));
                            var zz = f;
                            _volume.SetValue(xx, (330 - zz * 2 ), yy, i);
                            _volume.SetValue(xx, (330 - zz * 2 + 1), yy, i);
                        }

                    }
                }
                fs.Close();
                GC.Collect();
            }
            GC.Collect();
            _cameraXPositionFactor = 4;
            _cameraYPositionFactor = 2;
            _cameraZPositionFactor = 2;
        }


        private void LoadBrainVolumeData()
        {
            _volume = new Volume(320, _oclMan);
            _boxmin = new Float4(0, 0, 0, 0);
            _boxmax = new Float4(320, 320, 320, 0);

            for (var f = 1; f <= 109; f++)
            {
                var fs = new FileStream("MRbrain/MRbrain." + f, FileMode.Open, FileAccess.Read);

                var readbytes = 0;
                var buffer = new byte[2 * 256 * 256];
                while (readbytes < buffer.Length)
                {
                    readbytes += fs.Read(buffer, readbytes, buffer.Length - readbytes);
                }

                for (var y = 0; y < 256; y++)
                {
                    for (var x = 0; x < 256; x++)
                    {
                        var b = new byte[2];
                        b[1] = buffer[((x * 256 + y) * 2)];
                        b[0] = buffer[((x * 256 + y) * 2) + 1];
                        short i = BitConverter.ToInt16(b, 0);

                        if (i > 1500 && i < 4000)
                        {
                            var xx = (int)((x / 256.0) * (257.0 - 5));
                            var yy = 256 - (int)((y / 256.0) * (257.0 - 5));
                            var zz = f;
                            _volume.SetValue(50 + zz*2, 256 - xx, yy, i);
                            _volume.SetValue(50 + zz*2+1, 256 - xx, yy, i);
                        }

                    }
                }
                fs.Close();
                GC.Collect();
            }
            GC.Collect();
            _cameraXPositionFactor = 2;
            _cameraYPositionFactor = 2;
            _cameraZPositionFactor = 2;
        }

        private void LoadStentVolumeData()
        {
            _volume = new Volume(530, _oclMan);
            _boxmin = new Float4(0, 0, 0, 0);
            _boxmax = new Float4(512, 512, 512, 0);

            var fs = new FileStream("stent/stent16.raw", FileMode.Open, FileAccess.Read);

            var readbytes = 0;
            var buffer = new byte[512 * 512 * 174 * 2];
            while (readbytes < buffer.Length)
            {
                readbytes += fs.Read(buffer, readbytes, buffer.Length - readbytes);
            }
            fs.Close();

            for (var z = 0; z < 174; z++)
            {
                for (var y = 0; y < 512; y++)
                {
                    for (var x = 0; x < 512; x++)
                    {
                        var b = new byte[2];
                        b[1] = buffer[((x * 512 + y + z * 512 * 512) * 2 + 1)];
                        b[0] = buffer[((x * 512 + y + z * 512 * 512) * 2)];
                        short i = BitConverter.ToInt16(b, 0);

                        if (i > 500 && i < 4000)
                        {
                            var xx = (int)((x / 512.0) * (512.0 - 5));
                            var yy = 512 - (int)((y / 512.0) * (512.0 - 5));
                            var zz = z * 3;
                            _volume.SetValue(xx, zz, yy, i);
                            _volume.SetValue(xx, zz + 1, yy, i);
                            _volume.SetValue(xx, zz + 2, yy, i);                            
                        }

                    }
                }

            }

            GC.Collect();
            _cameraXPositionFactor = 4;
            _cameraYPositionFactor = 2;
            _cameraZPositionFactor = 2;
        }

        private void DoRayCasting(BitmapData output)
        {            
            Mem outputBuffer = null;
            try
            {
                int deviceIndex = comboBoxDeviceSelector.SelectedIndex;
                outputBuffer = _oclMan.Context.CreateBuffer(MemFlags.USE_HOST_PTR, output.Stride*output.Height,
                                                           output.Scan0);
                _cameraPosition = new Float4()
                                     {
                                         S0 =
                                             _volume.GetSize()/2 -
                                             (float) Math.Cos(_cameraAngle*Math.PI/180)*_cameraDistance,
                                         S1 = _volume.GetSize()/2,
                                         S2 =
                                             _volume.GetSize()/2 -
                                             (float) Math.Sin(_cameraAngle*Math.PI/180)*_cameraDistance,
                                         S3 = 0
                                     };

                _cameraLookAt = new Float4()
                {
                    S0 = _volume.GetSize() / _cameraXPositionFactor,
                    S1 = _volume.GetSize() / _cameraYPositionFactor,
                    S2 = _volume.GetSize() / _cameraZPositionFactor,
                    S3 = 0
                };

                _cameraForward = _cameraLookAt.Sub(_cameraPosition).Normalize();
                var down = new Float4(0.0f, -1.0f, 0.0f, 0.0f);
                var right = Helper.Cross(_cameraForward, down).Normalize().Times(1.5f);
                var up = Helper.Cross(_cameraForward, right).Normalize().Times(-1.5f);

                for (var x = 0; x < output.Width; x += OpenClBlocksize)
                {
                    for (var y = 0; y < output.Height; y += OpenClBlocksize)
                    {
                        var rayTracingGlobalWorkSize = new IntPtr[2];
                        rayTracingGlobalWorkSize[0] =
                            (IntPtr) (output.Width - x > OpenClBlocksize ? OpenClBlocksize : output.Width - x);
                        rayTracingGlobalWorkSize[1] =
                            (IntPtr) (output.Height - y > OpenClBlocksize ? OpenClBlocksize : output.Height - y);
                        var rayTracingGlobalOffset = new IntPtr[2];
                        rayTracingGlobalOffset[0] = (IntPtr) x;
                        rayTracingGlobalOffset[1] = (IntPtr) y;

                        _rayCastKernel.SetArg(0, output.Width);
                        _rayCastKernel.SetArg(1, output.Height);
                        _rayCastKernel.SetArg(2, outputBuffer);
                        _rayCastKernel.SetArg(3, output.Stride);
                        _rayCastKernel.SetArg(4, _cameraPosition);
                        _rayCastKernel.SetArg(5, _cameraForward);
                        _rayCastKernel.SetArg(6, right);
                        _rayCastKernel.SetArg(7, up);
                        _rayCastKernel.SetArg(8, _volume.GetBuffer());
                        _rayCastKernel.SetArg(9, _volume.GetSize());
                        _rayCastKernel.SetArg(10, _lightPosition);
                        _rayCastKernel.SetArg(11, _boxmin);
                        _rayCastKernel.SetArg(12, _boxmax);                        
                        _rayCastKernel.SetArg(13, _gradientType);
                        _rayCastKernel.SetArg(14, (short)colorMin.Value);
                        _rayCastKernel.SetArg(15, (short)colorMax.Value);
                        _rayCastKernel.SetArg(16, CutCheckBox.Checked ? (short)1 : (short)0);

                        _oclMan.CQ[deviceIndex].EnqueueNDRangeKernel(_rayCastKernel, 2, rayTracingGlobalOffset,
                                                                    rayTracingGlobalWorkSize, null);
                    }
                }

                _oclMan.CQ[deviceIndex].EnqueueBarrier();
                IntPtr p = _oclMan.CQ[deviceIndex].EnqueueMapBuffer(outputBuffer, true, MapFlags.READ, IntPtr.Zero,
                                                                   (IntPtr) (output.Stride*output.Height));
                _oclMan.CQ[deviceIndex].EnqueueUnmapMemObject(outputBuffer, p);
                _oclMan.CQ[deviceIndex].Finish();
            }
            catch(Exception ex)
            {
                MessageBox.Show("Exception occured during DoRayCasting:" + ex.Message, "Exception");
                Environment.Exit(-1);
            }
            finally
            {
                if(outputBuffer != null)
                {
                    outputBuffer.Dispose();
                }    
            }
            
        }

        private void CasterForm_Load(object sender, EventArgs e)
        {
            try
            {
                InitializeOpenCL();
                LoadBrainVolumeData();
                _initialized = true;                
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message, "Initiation/Volumedataloading failed");
                Application.Exit();
            }
        }

        private void InitializeOpenCL()
        {
            if (OpenCL.NumberOfPlatforms == 0)
            {
                MessageBox.Show("OpenCL not available!");
                Application.Exit();
            }

            _oclMan = new OpenCLManager();
            _oclMan.AttemptUseBinaries = true;
            _oclMan.AttemptUseSource = true;
            _oclMan.RequireImageSupport = false;
            _oclMan.BuildOptions = "";

            _oclMan.CreateDefaultContext(0, DeviceType.ALL);
            _oclProgram = _oclMan.CompileSource(Properties.Resources.RayCaster);

            for (int i = 0; i < _oclMan.Context.Devices.Length; i++)
                comboBoxDeviceSelector.Items.Add(_oclMan.Context.Devices[i].Vendor + ":" + _oclMan.Context.Devices[i].Name);
            comboBoxDeviceSelector.SelectedIndex = 0;

            _rayCastKernel = _oclProgram.CreateKernel("RayCaster");
        }

        private void DrawPanel_Paint(object sender, PaintEventArgs e)
        {
            _drawingStopwatch.Restart();

            /*if (OutputBitmap == null || (OutputBitmap.Width != DrawPanel.Width && OutputBitmap.Height != DrawPanel.Height))
            {
                OutputBitmap = new Bitmap(DrawPanel.Width, DrawPanel.Height, PixelFormat.Format32bppArgb);               
            }*/

            Graphics g = e.Graphics;

            if (_initialized)
            {
                Draw();
                g.DrawImage(_outputBitmap, 0, 0, DrawPanel.Width, DrawPanel.Height);
            }
            _drawingStopwatch.Stop();
            var fps = 1000.0 / _drawingStopwatch.ElapsedMilliseconds;
            Text = string.Format("GPUTracer - FPS: {1} - Drawing Image Time: {0} ms",_drawingStopwatch.ElapsedMilliseconds, fps);
        }

        internal void Idle()
        {

            short state = NativeMethods.GetKeyState((int)Keys.Left);
            bool leftKeyDown = NativeMethods.HIWORD(state) != 0;
            state = NativeMethods.GetKeyState((int)Keys.Right);
            bool rightKeyDown = NativeMethods.HIWORD(state) != 0;
            state = NativeMethods.GetKeyState((int)Keys.Up);
            bool upKeyDown = NativeMethods.HIWORD(state) != 0;
            state = NativeMethods.GetKeyState((int)Keys.Down);
            bool downKeyDown = NativeMethods.HIWORD(state) != 0;

            if(leftKeyDown)
            {
                RotateCameraLeft();
            }
            if (rightKeyDown)
            {
                RotateCameraRight();
            }

            if (downKeyDown)
            {
                ZoomCameraIn();
            }

            if (upKeyDown)
            {
                ZoomCameraOut();
            }    

            if (_initialized)
            {
                Draw();
                DrawPanel.Refresh();
            }
        }

        private void ZoomCameraOut()
        {
            _cameraDistance -= 10;
            if (_cameraDistance <= 0)
            {
                _cameraDistance = 0;
            }
        }

        private void ZoomCameraIn()
        {
            _cameraDistance += 10;
            if (_cameraDistance >= 1200)
            {
                _cameraDistance = 1200;
            }
        }

        private void RotateCameraRight()
        {
            _cameraAngle += 10;
            if (_cameraAngle >= 360)
            {
                _cameraAngle = 0;
            }
        }

        private void RotateCameraLeft()
        {
            _cameraAngle -= 10;
            if (_cameraAngle <= 0)
            {
                _cameraAngle = 360;
            }
        }

        private void MinBoxX_Scroll(object sender, EventArgs e)
        {
            _boxmin = new Float4(MinBoxX.Value, _boxmin.S1,_boxmin.S2,0);
            UpdateBoundingBoxLabels();
        }

        private void MaxBoxX_Scroll(object sender, EventArgs e)
        {
            _boxmax = new Float4(MaxBoxX.Value, _boxmax.S1, _boxmax.S2, 0);
            UpdateBoundingBoxLabels();
        }

        private void MinBoxY_Scroll(object sender, EventArgs e)
        {
            _boxmin = new Float4(_boxmin.S0, MinBoxY.Value, _boxmin.S2, 0);
            UpdateBoundingBoxLabels();
        }

        private void MaxBoxY_Scroll(object sender, EventArgs e)
        {
            _boxmax = new Float4(_boxmax.S0, MaxBoxY.Value, _boxmax.S2, 0);
            UpdateBoundingBoxLabels();
        }

        private void MinBoxZ_Scroll(object sender, EventArgs e)
        {
            _boxmin = new Float4(_boxmin.S0, _boxmin.S1, MinBoxZ.Value, 0);
            UpdateBoundingBoxLabels();
        }

        private void MaxBoxZ_Scroll(object sender, EventArgs e)
        {
            _boxmax = new Float4(_boxmax.S0, _boxmax.S1, MaxBoxZ.Value, 0);
            UpdateBoundingBoxLabels();
        }

        private void DrawPanel_Click(object sender, EventArgs e)
        {
            DrawPanel.Focus();
        }

        private void CentralDifferenceRadioButtonCheckedChanged(object sender, EventArgs e)
        {           
            if(CentralDifferenceOperatorRadioButton.Checked)
                _gradientType = 0;            
        }

        private void ZuckerHummelRadioButtonCheckedChanged(object sender, EventArgs e)
        {
            if (ZuckerHummelOperatorRadioButton.Checked)
                _gradientType = 1;           
        }

        private void Sobel3DRadioButtonCheckedChanged(object sender, EventArgs e)
        {
            if (Sobel3DOperatorRadioButton.Checked)
                _gradientType = 2;            
        }

        private void LightX_Scroll(object sender, EventArgs e)
        {
            _lightPosition.S0 = LightX.Value;
            UpdateLightVectorLabel();
        }

        private void LightY_Scroll(object sender, EventArgs e)
        {
            _lightPosition.S1 = LightY.Value;
            UpdateLightVectorLabel();
        }

        private void LightZ_Scroll(object sender, EventArgs e)
        {
            _lightPosition.S2 = LightZ.Value;
            UpdateLightVectorLabel();
        }

        private void UpdateLightVectorLabel()
        {
            LightVectorLabel.Text = String.Format("({0},{1},{2})",_lightPosition.S0,_lightPosition.S1,_lightPosition.S2);
        }

        private void UpdateBoundingBoxLabels()
        {
            BoundingBoxMinLabel.Text = String.Format("({0},{1},{2})", _boxmin.S0, _boxmin.S1, _boxmin.S2);
            BoundingBoxMaxLabel.Text = String.Format("({0},{1},{2})", _boxmax.S0, _boxmax.S1, _boxmax.S2);
        }

        private void radioButton1_CheckedChanged_1(object sender, EventArgs e)
        {
            if (Resolution1RadioButton.Checked)
            {
                _outputBitmap = new Bitmap(512, 384, PixelFormat.Format32bppArgb);
            }
        }

        private void radioButton2_CheckedChanged_1(object sender, EventArgs e)
        {
            if (Resolution2RadioButton.Checked)
            {
                _outputBitmap = new Bitmap(640, 480, PixelFormat.Format32bppArgb);
            }
        }

        private void radioButton3_CheckedChanged(object sender, EventArgs e)
        {
            if (Resolution3RadioButton.Checked)
            {
                _outputBitmap = new Bitmap(800, 600, PixelFormat.Format32bppArgb);
            }
        }

        private void radioButton4_CheckedChanged(object sender, EventArgs e)
        {
            if (Resolution4RadioButton.Checked)
            {
                _outputBitmap = new Bitmap(1024, 768, PixelFormat.Format32bppArgb);
            }
        }

        private void LoadBunnyButton_Click(object sender, EventArgs e)
        {
            LoadBunnyVolumeData();
        }

        private void LoadHeadVolumeButton_Click(object sender, EventArgs e)
        {
            LoadHeadVolumeData();
        }

        private void LoadBrainVolumeButton_Click(object sender, EventArgs e)
        {
            LoadBrainVolumeData();
        }

        private void LoadStentVolumeButton_Click(object sender, EventArgs e)
        {
            LoadStentVolumeData();
        }      

        private void DrawPanel_MouseMove(object sender, MouseEventArgs e)
        {
            if(e.Button == MouseButtons.Left)
            {
                if (_oldPosition != null)
                {
                    int horizontalMovement = e.Location.X - _oldPosition.X;
                    if (horizontalMovement > 0)
                    {                        
                        RotateCameraLeft();
                    }
                    if (horizontalMovement < 0)
                    {
                        RotateCameraRight();
                    }
                }
                _oldPosition = e.Location;
            }
        }

        private void DrawPanel_MouseWheel(object sender, MouseEventArgs e)
        {
            if (e.Delta > 0)
            {
                for (var repeat = 0; repeat < 3; repeat++)
                    ZoomCameraOut();
            }
            if (e.Delta < 0)
            {
                for(var repeat = 0; repeat < 3; repeat++)
                    ZoomCameraIn();
            }
        }         
    }

    public class DrawPanel : Panel
    {
        public DrawPanel()
            : base()
        {
            //The trick here to avoid "flickering": We enable double buffering
            //but this enabling is only possible in derived panels because the DoubleBuffer
            //is protected
            SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
            SetStyle(ControlStyles.UserPaint, true);
            SetStyle(ControlStyles.AllPaintingInWmPaint, true);
            UpdateStyles();
        }
    }

    [ComVisible(false), SuppressUnmanagedCodeSecurity()]
    internal class NativeMethods
    {
        [DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true, CallingConvention = CallingConvention.Winapi)]
        public static extern short GetKeyState(int keyCode);

        public static int HIWORD(int n)
        {
            return ((n >> 16) & 0xffff/*=~0x0000*/);
        }

        public static int LOWORD(int n)
        {
            return (n & 0xffff/*=~0x0000*/);
        }
    } 
}
